package com.dtflys.easyel.parser;

import com.dtflys.easyel.compile.EasyElSource;
import com.dtflys.easyel.compile.EasyElSourceLine;
import com.dtflys.easyel.exception.EasyElException;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import static com.dtflys.easyel.parser.EToken.*;

public class ELexer {
    
    private final static Map<String, Integer> keywordTable = new HashMap<>();
    static {
        keywordTable.put("null", NULL);
        keywordTable.put("new", NEW);
        keywordTable.put("true", TRUE);
        keywordTable.put("false", FALSE);
        keywordTable.put("in", IN);
        keywordTable.put("!in", NOT_IN);
        keywordTable.put("not", NOT);
        keywordTable.put("and", AND);
        keywordTable.put("or", OR);
        keywordTable.put("if", IF);
        keywordTable.put("else", ELSE);
        keywordTable.put("for", FOR);
        keywordTable.put("do", DO);
        keywordTable.put("while", WHILE);
        keywordTable.put("break", BREAK);
        keywordTable.put("continue", CONTINUE);
        keywordTable.put("return", RETURN);
        keywordTable.put("let", LET);
        keywordTable.put("var", VAR);
    }
    
    private final EasyElSource source;
    
    private int currentColumn = 0;
    
    private EasyElSourceLine lastLine = null;
    
    private EasyElSourceLine currentLine = null;
    
    private char[] currentLineChars = null;
    
    private char current = 0;
    
    private final char EOF = (char) -1;
    
    
    public static class TokenContext {
        EasyElSource source;
        EasyElSourceLine sourceLine;
        int line = 0;
        int startColumn;
        int endColumn;
        StringBuilder buffer = new StringBuilder();
        
        void record(char current, int column) {
            buffer.append(current);
            endColumn = column;
        }
        
        String getText() {
            return buffer.toString();
        }
        
        EToken token(int type) {
            return new EToken(type, this);
        }
    }

    public EasyElSource getSource() {
        return source;
    }

    private TokenContext createContext() {
        TokenContext context = new TokenContext();
        context.source = source;
        if (currentLine == null) {
            return null;
        }
        context.line = currentLine.getLineNumber();
        context.startColumn = currentColumn;
        context.endColumn = currentColumn;
        return context;
    }
    
    private void record(TokenContext context) {
        context.record(current, currentColumn);
    }

    public ELexer(EasyElSource source) {
        this.source = source;
    }
    
    private void readNextLine() throws IOException {
        if (currentLine != null) {
            lastLine = currentLine;            
        }
        currentLine = source.nextLine();
        if (currentLine != null) {
            currentLineChars = currentLine.getChars();
        } else {
            currentLineChars = null;
        }
        currentColumn = 0;
    }
    
    private void nextChar() throws IOException {
        if (currentLine == null || currentColumn >= currentLineChars.length) {
            readNextLine();
        }
        if (currentLineChars == null || currentLineChars.length == 0) {
            current = EOF;
        } else {
            current = currentLineChars[currentColumn++];
        }
    }
    
    private void nextChar(TokenContext context) throws IOException {
        record(context);
        nextChar();
    }
    
    public char watch(int n) throws IOException {
        if (currentColumn + n - 1 < currentLineChars.length) {
            return currentLineChars[currentColumn + n - 1];
        }
        return '\n';
    }
    
    public boolean match(final TokenContext context, final char ch) throws IOException {
        if (current == ch) {
            nextChar(context);
            return true;
        }
        throw new EasyElException("Unexpected character '" + ch + "' at line " + currentLine.getLineNumber());
    }
    
    public EToken nextToken() throws IOException {
        return nextToken(true);
    }
    
    public EToken nextToken(boolean skipWhitespace) throws IOException {
        if (currentColumn == 0) {
            nextChar();
        }

        for ( ; ;) {
            final TokenContext context = createContext();
            if (context == null) {
                return handleEOF();
            }
            final EToken token = tokenize(context);
            if (token == null) {
                return handleEOF(context);
            }
            if (skipWhitespace && token.skip) {
                continue;
            }
            return token;
        }
    }
    
    public EToken handleEOF() {
        return handleEOF(null);
    }
    
    private EToken handleEOF(final TokenContext context) {
        if (context == null) {
            return new EToken(source, EToken.EOF, "EOF", lastLine.getLineNumber(), currentColumn, currentColumn);
        }
        return new EToken(source, EToken.EOF, "EOF", context.line, context.startColumn, context.endColumn);
    }
    
    private EToken tokenize(final TokenContext context) throws IOException {
        switch (current) {
            case EOF:
                return handleEOF(context);

            case ' ':
            case '\t':
                // whitespace
                do {
                    nextChar(context);
                } while (current == ' ' || current == '\t');
                return context.token(WS).skip();

            case '\r':
            case '\n':
                // next line
                nextChar(context);
                return context.token(NL);

            case '.':
                nextChar(context);
                if (current == '.') {
                    // double dot
                    nextChar(context);
                    return context.token(DOUBLE_DOT);
                }
                // dot
                return context.token(DOT);
            
            case '!': {
                nextChar(context);
                if (current == '=') {
                    // != (not equals)
                    nextChar(context);
                    if (current == '~') {
                        // !=~ (not regex match)
                        nextChar(context);
                        return context.token(NOT_REGEX_MATCH);
                    }
                    return context.token(NOT_EQ);
                }
                
                if (current == 'i' && watch(1) == 'n' && !isIdentityChar(watch(2))) {
                    nextChar(context);
                    nextChar(context);
                    // !in (not in)
                    return context.token(NOT_IN);
                }
                
                // not
                return context.token(NOT);
            }

            case '?':
                nextChar(context);
                if (current == '?') {
                    // ?? (double question)
                    nextChar(context);
                    return context.token(DOUBLE_QUESTION);
                }
                // question
                return context.token(QUESTION);

            case ':':
                nextChar(context);
                return context.token(COLON);

            case ',':
                nextChar(context);
                return context.token(COMMA);
                
            case ';':
                nextChar(context);
                return context.token(SEMICOLON);

            case '=':
                nextChar(context);
                switch (current) {
                    case '=':
                        // equals
                        nextChar(context);
                        return context.token(EQ);
                    case '~':
                        // regex match
                        nextChar(context);
                        return context.token(REGEX_MATCH);
                    default:
                        // assignment
                        return context.token(ASSIGN);
                }

            case '<':
                nextChar(context);
                if (current == '=') {
                    // less or equals
                    nextChar(context);
                    return context.token(LE);
                }
                // less than
                return context.token(LT);

            case '>':
                nextChar(context);
                if (current == '=') {
                    // greater or equals
                    nextChar(context);
                    return context.token(GE);
                }
                // greater than
                return context.token(GT);

            case '(':
                nextChar(context);
                return context.token(LPAREN);

            case ')':
                nextChar(context);
                return context.token(RPAREN);
                
            case '[':
                nextChar(context);
                return context.token(LBRACK);
                
            case ']':
                nextChar(context);
                return context.token(RBRACK);

            case '{':
                nextChar(context);
                return context.token(LBRACE);

            case '}':
                nextChar(context);
                return context.token(RBRACE);

            case '+':
                nextChar(context);
                switch (current) {
                    case '+':
                        // increment
                        nextChar(context);
                        return context.token(PLUS_PLUS);
                    case '=':
                        // plus and assign
                        nextChar(context);
                        return context.token(PLUS_ASSIGN);
                    default:
                        // minus
                        return context.token(PLUS);
                }

            case '-':
                nextChar(context);
                switch (current) {
                    case '-':
                        // decrement
                        nextChar(context);
                        return context.token(MINUS_MINUS);
                    case '=':
                        // minus and assign
                        nextChar(context);
                        return context.token(MINUS_ASSIGN);
                    case '>':
                        // right arrow
                        nextChar(context);
                        return context.token(RIGHT_ARROW);
                    default:
                        // minus
                        return context.token(MINUS);
                }

            case '*':
                nextChar(context);
                if (current == '*') {
                    // double star
                    nextChar(context);
                    return context.token(DOUBLE_STAR);
                }

                // multiple
                return context.token(MUL);

            case '/': {
                char ch = watch(1);
                if (ch == '/') {
                    // single line comment
                    return singleLineComment(context);
                }

                if (ch == '*') {
                    // multiple lines comment
                    return multipleLinesComment(context);
                }

                int n = 2;
                if (isRegexCharacter(ch)) {
                    do {
                        ch = watch(n++);
                    } while (isRegexCharacter(ch));
                    if (ch == '/') {
                        for (int i = 0; i < n; i++) {
                            nextChar(context);
                        }
                        return context.token(REGEX_PATTERN);
                    }
                }

                // division
                nextChar(context);
                return context.token(DIV);
            }
            
            case '"': {
                // double quote string
                nextChar(context);
                if (isDoubleQuoteStringChar(current)) {
                    do {
                        nextChar(context);
                    } while (isDoubleQuoteStringChar(current));
                    match(context, '"');
                    return context.token(DOUBLE_QUOTE_STRING);
                }
            }
            
            case '\'': {
                // single quote string
                nextChar(context);
                if (isSingleQuoteStringChar(current)) {
                    do {
                        nextChar(context);
                    } while (isSingleQuoteStringChar(current));
                    match(context, '\'');
                    return context.token(SINGLE_QUOTE_STRING);
                }
            }
            
            case '0':
            case '1':
            case '2':
            case '3':
            case '4':
            case '5':
            case '6':
            case '7':
            case '8':
            case '9':
                return digit(context);

            case '_':
            case '$':
            case 'a':
            case 'b':
            case 'c':
            case 'd':
            case 'e':
            case 'f':
            case 'g':
            case 'h':
            case 'i':
            case 'j':
            case 'k':
            case 'l':
            case 'm':
            case 'n':
            case 'o':
            case 'p':
            case 'q':
            case 'r':
            case 's':
            case 't':
            case 'u':
            case 'v':
            case 'w':
            case 'x':
            case 'y':
            case 'z':
            case 'A':
            case 'B':
            case 'C':
            case 'D':
            case 'E':
            case 'F':
            case 'G':
            case 'H':
            case 'I':
            case 'J':
            case 'K':
            case 'L':
            case 'M':
            case 'N':
            case 'O':
            case 'P':
            case 'Q':
            case 'R':
            case 'S':
            case 'T':
            case 'U':
            case 'V':
            case 'W':
            case 'X':
            case 'Y':
            case 'Z':
                return keywordOrIdentifier(context);

            default:
                return null;
        }
    }
    
    
    private boolean isHexChar() {
        return Character.isDigit(current) || (current >= 'a' && current <= 'f') || (current >= 'A' && current <= 'F');
    }
    
    private boolean isIdentityChar(final char ch) {
        return Character.isLetter(ch) || Character.isDigit(ch) || ch == '_' || ch == '$';
    }
    
    private boolean isRegexCharacter(final char ch) {
        return ch != '/' && ch != EOF && ch != '\n' && ch != '\r';
    }
    
    private boolean isDoubleQuoteStringChar(final char ch) {
        return ch != '"' && ch != '\r' && ch != '\n';
    }

    private boolean isSingleQuoteStringChar(final char ch) {
        return ch != '\'' && ch != '\r' && ch != '\n';
    }


    private EToken keywordOrIdentifier(final TokenContext context) throws IOException {
        nextChar(context);
        if (isIdentityChar(current)) {
            do {
                nextChar(context);
            } while (isIdentityChar(current));
        }
        
        final int type = keywordTable.computeIfAbsent(context.getText(), key -> IDENTIFIER);
        return context.token(type);
    }
    
    private EToken digit(final TokenContext context) throws IOException {
        if (current == '0') {
            // zero
            nextChar(context);
            if (current == 'x' || current == 'X') {
                // hex
                nextChar(context);
                if (isHexChar()) {
                    do {
                        nextChar(context);
                    } while (isHexChar());
                    return context.token(HEX);
                }
            } else if (Character.isLetter(current)) {
                // oct
                do {
                    nextChar(context);
                } while (Character.isDigit(current));
                return context.token(OCT);
            }
        } else {
            do {
                nextChar(context);
            } while (Character.isDigit(current));
        }

        if (current == '.') {
            // decimal
            if (Character.isDigit(watch(1))) {
                nextChar(context);
                do {
                    nextChar(context);
                } while (Character.isDigit(current));
                return context.token(DECIMAL);
            }
        } 
        // integer
        return context.token(INTEGER);
    }
    
    
    private EToken singleLineComment(final TokenContext context) throws IOException {
        nextChar(context);
        if (current != '\n' && current != '\r' && current != EOF) {
            do {
                nextChar(context);
            } while (current != '\n' && current != '\r' && current != EOF);
        }
        return context.token(SINGLE_LINE_COMMENT);
    }

    private EToken multipleLinesComment(final TokenContext context) throws IOException {
        nextChar(context);
        if (current != EOF) {
            do {
                if (current == '*') {
                    if (watch(1) == '/') {
                        nextChar(context);
                        return context.token(MULTIPLE_LINES_COMMENT).skip();
                    }
                }
                nextChar(context);
            } while (current != EOF);
        }
        return context.token(MULTIPLE_LINES_COMMENT).skip();
    }

}
